﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using élénkPróbája.helpers;
using élénkPróbája.results;

namespace élénkPróbája.validators
{
    public class DelegateValidator<T> : IValidationRule
    {
        private readonly Func<T, LitmusValidationContext<T>, IEnumerable<ValidationFailure>> func;
        private readonly Func<T, LitmusValidationContext<T>, CancellationToken, Task<IEnumerable<ValidationFailure>>> asyncFunc;

        // Work-around for reflection bug in .NET 4.5
        static Func<object, bool> s_condition = x => true;
        private Func<object, bool> condition = s_condition;
        private Func<object, Task<bool>> asyncCondition = null;

        /// <summary>
        /// Rule set to which this rule belongs.
        /// </summary>
        public string RuleSet { get; set; }

        /// <summary>
        /// Creates a new DelegateValidator using the specified function to perform validation.
        /// </summary>
        public DelegateValidator(Func<T, LitmusValidationContext<T>, IEnumerable<ValidationFailure>> func)
        {
            this.func = func;
            asyncFunc = (x, ctx, cancel) => TaskHelpers.RunSynchronously(() => this.func(x, ctx), cancel);
        }

        /// <summary>
        /// Creates a new DelegateValidator using the specified function to perform validation.
        /// </summary>
        public DelegateValidator(Func<T, IEnumerable<ValidationFailure>> func)
            : this((x, ctx) => func(x))
        {
        }

        /// <summary>
        /// Creates a new DelegateValidator using the specified async function to perform validation.
        /// </summary>
        public DelegateValidator(Func<T, LitmusValidationContext<T>, CancellationToken, Task<IEnumerable<ValidationFailure>>> asyncFunc)
        {
            this.asyncFunc = asyncFunc;
            func = (x, ctx) => Task.Factory.StartNew(() => this.asyncFunc(x, ctx, new CancellationToken())).Unwrap().Result;
        }

        /// <summary>
        /// Creates a new DelegateValidator using the specified async function to perform validation.
        /// </summary>
        public DelegateValidator(Func<T, Task<IEnumerable<ValidationFailure>>> asyncFunc)
            : this((x, ctx, cancel) => asyncFunc(x))
        {
        }

        /// <summary>
        /// Performs validation using a validation context and returns a collection of Validation Failures.
        /// </summary>
        /// <param name="context">Validation Context</param>
        /// <returns>A collection of validation failures</returns>
        public IEnumerable<ValidationFailure> Validate(LitmusValidationContext<T> context)
        {
            return func(context.InstanceToValidate, context);
        }

        /// <summary>
        /// Performs validation asynchronously using a validation context and returns a collection of Validation Failures.
        /// </summary>
        /// <param name="context">Validation Context</param>
        /// <returns>A collection of validation failures</returns>
        public Task<IEnumerable<ValidationFailure>> ValidateAsync(LitmusValidationContext<T> context, CancellationToken cancellation)
        {
            return asyncFunc(context.InstanceToValidate, context, cancellation);
        }

        /// <summary>
        /// The validators that are grouped under this rule.
        /// </summary>
        public IEnumerable<ILitPropertyValidator> Validators
        {
            get { yield break; }
        }

        /// <summary>
        /// Performs validation using a validation context and returns a collection of Validation Failures.
        /// </summary>
        /// <param name="context">Validation Context</param>
        /// <returns>A collection of validation failures</returns>
        public IEnumerable<ValidationFailure> Validate(ValidationContext context)
        {
            if (!context.Selector.CanExecute(this, "", context) || !condition(context.InstanceToValidate) ||
                (asyncCondition != null && !asyncCondition(context.InstanceToValidate).Result))
            {
                return Enumerable.Empty<ValidationFailure>();
            }

            var newContext = new LitmusValidationContext<T>((T)context.InstanceToValidate, context.PropChain, context.Selector);
            return Validate(newContext);
        }

        /// <summary>
        /// When overloaded performs validation asynchronously using a validation context and returns a collection of Validation Failures.
        /// </summary>
        /// <param name="context">Validation Context</param>
        /// <returns>A collection of validation failures</returns>
        public Task<IEnumerable<ValidationFailure>> ValidateAsync(ValidationContext context, CancellationToken cancellation)
        {
            if (!context.Selector.CanExecute(this, "", context) || !condition(context.InstanceToValidate))
            {
                return TaskHelpers.FromResult(Enumerable.Empty<ValidationFailure>());
            }

            return asyncCondition == null
                ? ValidateAsyncInternal(context, cancellation)
                : asyncCondition(context.InstanceToValidate).Then(shouldValidate =>
                    shouldValidate
                        ? ValidateAsyncInternal(context, cancellation)
                        : TaskHelpers.FromResult(Enumerable.Empty<ValidationFailure>()),
                    runSynchronously: true, cancellationToken: cancellation);
        }

        Task<IEnumerable<ValidationFailure>> ValidateAsyncInternal(ValidationContext context, CancellationToken cancellation)
        {
            var newContext = new LitmusValidationContext<T>((T)context.InstanceToValidate, context.PropChain, context.Selector);
            return ValidateAsync(newContext, cancellation);
        }

        public void ApplyCondition(Func<object, bool> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators)
        {
            // For custom rules within the DelegateValidator, we ignore ApplyConditionTo - this is only relevant to chained rules using RuleFor.
            var originalCondition = this.condition;
            this.condition = x => predicate(x) && originalCondition(x);
        }

        public void ApplyAsyncCondition(Func<object, Task<bool>> predicate, ApplyConditionTo applyConditionTo = ApplyConditionTo.AllValidators)
        {
            // For custom rules within the DelegateValidator, we ignore ApplyConditionTo - this is only relevant to chained rules using RuleFor.
            var originalCondition = this.asyncCondition;
            this.asyncCondition = x => predicate(x).Then(result => {
                if (!result)
                    return TaskHelpers.FromResult(false);

                if (originalCondition == null)
                    return TaskHelpers.FromResult(true);

                return originalCondition(x);
            },
                runSynchronously: true);
        }
    }
}